iOS 事件处理(2)

接上面

具体例子

用自定义触摸事件来实现相关 UIGestureRecognizer。这一小节的详细代码可见Multitouch EventsListing 3-1Listing 3-7

处理点击手势

用 UITouch 的 tapCount 属性来判断是单击还是双击还是三击。最好是在 touchesEnded:withEvent: 方法里面做判断处理,因为它是用户手指离开 App 时才响应的,要确保它真的是个 tap 手势,而不是拖动啥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *aTouch in touches) {
if (aTouch.tapCount >= 2) {
// The view responds to the tap
[self respondToDoubleTapGesture:aTouch];
}
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}

处理滑动和拖动手势

滑动手势

从三个角度判断它是否是一个滑动手势

  • Did the user’s finger move far enough?
  • Did the finger move in a relatively straight line?
  • Did the finger move quickly enough to call it a swipe?

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define HORIZ_SWIPE_DRAG_MIN 12
#define VERT_SWIPE_DRAG_MAX 4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
// startTouchPosition is a property
self.startTouchPosition = [aTouch locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint currentTouchPosition = [aTouch locationInView:self];
// Check if direction of touch is horizontal and long enough
if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
{
// If touch appears to be a swipe
if (self.startTouchPosition.x < currentTouchPosition.x) {
[self myProcessRightSwipe:touches withEvent:event];
} else {
[self myProcessLeftSwipe:touches withEvent:event];
}
self.startTouchPosition = CGPointZero;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
self.startTouchPosition = CGPointZero;
}

拖动手势

简单的一根手指拖动的相关代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint loc = [aTouch locationInView:self];
CGPoint prevloc = [aTouch previousLocationInView:self];
CGRect myFrame = self.frame;
float deltaX = loc.x - prevloc.x;
float deltaY = loc.y - prevloc.y;
myFrame.origin.x += deltaX;
myFrame.origin.y += deltaY;
[self setFrame:myFrame];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}

多点触摸

taps,drags,swipes 通常都只涉及了一个 touch,比较简单去跟踪。但是处理由多个 touches 组成的触摸事件时,比较有挑战性。需要去记录 touch 的所有相关属性,并且改变它的 state 等等。需要做到两点:

  • Set the view’s multipleTouchEnabled property to YES;将多点触摸属性置为 YES;
  • Use a Core Foundation dictionary object (CFDictionaryRef) to track the mutations of touches through their phases during the event;用 CFDictionaryRef 来跟着 UITouch,这里用 CFDictionaryRef 而不是 NSDictionary,因为 NSDictionary 会 copy 它的 key。而 UITouch 没有采取 NSCopying 协议。

Determining when the last touch in a multitouch sequence has ended,判断 multitouch sequence 里的最后一个 touch 是否结束,可以用下面的代码

1
2
3
4
5
6
7
8
9
10
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if ([touches count] == [[event touchesForView:self] count]) {
// Last finger has lifted
}
}

指定自定义的触摸事件行为

通过改变一些属性去改变事件流的处理。

多个 touch 的分发传递(multipleTouchEnabled)

Turn on delivery of multiple touches. 默认值为NO,意味着只会接受触摸队列里面的第一个 touch,其他的会忽略掉。所以,[touches anyObject]方法就只会返回一个对象。将属性 multipleTouchEnabled 设为 YES,则可以处理多个 touches。

限制事件只分发给一个 view(exclusiveTouch)

Restrict event delivery to a single view. 即只有一个 view 响应事件。默认情况下,view 的 exclusiveTouch 属性为 NO,这就意味着,一个 view 不会阻塞 window里的其他 view 去接受事件。如果将某个 view 的 exclusiveTouch 设为 YES,那么当它接受 touches 时,只会有它一个接收 touches 。这里举例说明了 exclusiveTouch 属性,A、B、C 3个 view 多点触摸的例子。

Apple Restricting event delivery with an exclusive-touch view

If the user touches inside A, it recognizes the touch. But if a user holds one finger inside view B and also touches inside view A, then view A does not receive the touch because it was not the only view tracking touches. Similarly, if a user holds one finger inside view A and also touches inside view B, then view B does not receive the touch because view A is the only view tracking touches. At any time, the user can still touch both B and C, and those views can track their touches simultaneously.

exclusiveTouch 这个属性比较傲娇,只有当设置它的为 YES 的 view 首先收到触摸事件时,它才能响应。

限制事件分发到 subviews 上 (hitTest:withEvent:)

Restrict event delivery to subviews. 重载 hitTest:withEvent: 方法返回自己 self。

关闭事件的分发

  • userInteractionEnabled 属性置为 NO;
  • hidden 属性置为 NO;
  • alpha 属性值 <= 0.01;

阶段性的关闭事件的分发

beginIgnoringInteractionEvents 方法关闭,endIgnoringInteractionEvents 方法开启。这个方法是 UIApplication 的,所以能做一些全局性的事情。

触摸事件的转发

你可以将一个事件转发给另外一个响应对象(响应链就是这样玩的嘛),当你使用这个技术的时候得小心,因为 UIKit 没有设计去接受不属于它们的事件。所以,你不要转发给 UIKit 框架的对象。如果你想要有条件的去转发事件给其他响应对象时,那么这些对象应该是 UIView 的实例,并且这些对象关心事件的转发,并且能够处理这些事件。原因如下:

For a responder object to handle a touch, the touch’s view property must hold a reference to the responder. 一个 responder 对象想要处理一个 touch ,那么 touch 的 view 属性必须持有这个 responder。

事件的转发经常需要去分析 touch 对象觉得它是否应该转发事件。这里有一些方法你可以采取去分析:

  • With an “overlay” view, such as a common superview, use hit-testing to intercept events for analysis prior to forwarding them to subviews.(使用 overlay view,例如公用的父视图,在转发到 subviews 之前拦截事件去分析)
  • Override sendEvent: in a custom subclass of UIWindow, analyze touches, and forward them to the appropriate responders.(UIWindow 的子类重载 sendEvent: 方法,将事件转发到合适的 responders)

重载 sendEvent: 方法可以监听 App 事件的接收。UIApplication 和 UIWindow 都是用这个方法来分发事件的,所以它就是事件进入 App 的管道一样。当你重载的时候,务必调用父类的实现,[super sendEvent:event]。在 control 和 gesture recognizer 的响应事件里面打断点,可以看到,事件走的 UIKit 开始传递都是先走的,[UIApplication sendEvent:]、[UIWindow sendEvent:],最终都是走的 [UIApplication sendAction:to:from:forEvent:]。

gesture recognizer handle flow

control handle flow

处理多点触摸事件的最佳实践

当处理 touch 和 motion 事件时,这里有一些值得推荐的技巧和模式:

  • 记得实现事件的取消方法。
  • 如果自定义的是 UIView、UIViewController、UIResponder的子类时,你应该实现所有的事件方法,即使里面没有做任何实现。但是不要在里面调用父类的实现。
  • 如果是其他 UIKit 的子类时,你没有必须实现所有的事件方法。但是,你必须得调用父类的实现。即 [super touchesBegan:touches withEvent:event] 。
  • 只转发事件给 UIView 的子类。并确保这些转发后的对象能够知道并且处理这些不属于它的事件。
  • 不要显示的通过 nextResponder 方法在响应链上发送事件。相反的,调用父类的实现,并且让 UIKit 去遍历处理。
  • 不要使用 round-to-integer 代码(即不要使用 integer 来处理 float),这样会丢失精度。
  • 如果在事件处理的时候需要创建持久对象,记得在 touchesCancelled:withEvent 和 touchesEnded:withEvent: 里面销毁它们。
  • 如果你阻止它接受某个状态的 touch 事件时,可能导致结果不确定。不过,我们在实际中应该不会这样做。

需求

事件传递的最终目标是找到一个能够处理响应这个事件的对象(UIResponder 的子类)。如果找不到就丢弃它。

前提条件

能够处理事件的对象需要完成下面3个条件:

  • 实现这四个方法
1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
* 可以交互的。即 userInteractionEnabled 属性为 YES
* 是可见的。即 hidden = NO & alpha > 0.01

UIKit 已有的轮子

换汤不要药,跟前面的前提条件一样,只不过是另外一种形式来完成而已。

gesture recognizer

通过实现跟 UIResponder 相同签名的方法来完成。参考例子,上面有提到,官方文档 Listing 1-8 Implementation of a checkmark gesture recognizerYYGestureRecognizer

1
2
3
4
5
6
7
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

UIControl

也是内部实现跟 UIResponder 相同功能的方法来完成,里面通过一个 _targetActions 数组来存储各种 UIControlEvents 状态的事件。可以参考Chameleon UIControlSVSegmentedControl

1
2
3
4
5
6
7
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event;

过程

手指触摸屏幕就会生成 UIEvent 对象,然后放在 application 的队列里面,application 会从系统队列的顶层取出一个事件并分发它。application(sendEvent:) -> window(sendEvent:) -> initial object(hit-test view or frist responder)。
而 application 和 window 则是通过 Hit-Testing 和响应链来找到 initial object。一般情况下,都不需要我们去干涉 UIKit 的这个分发过程。但是,我们可以在这个过程去干涉达到自己的需求。

用途

这个章节的相关代码参考自

扩大触摸区域

我们绘制 UIButton 的时候,想要扩大它的响应区域。我们可以在 UIButton 里面处理 Hit-Testing 的那两个方法其中一个里面做处理。

hitTest:withEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGRect touchRect = CGRectInset(self.bounds, -10, -10);
if (CGRectContainsPoint(touchRect, point)) {
return self;
}
return [super hitTest:point withEvent:event];
}

pointInside:withEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}
CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
CGRect hitTestingBounds = bounds;
if (minimumHitTestWidth > bounds.size.width) {
hitTestingBounds.size.width = minimumHitTestWidth;
hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
}
if (minimumHitTestHeight > bounds.size.height) {
hitTestingBounds.size.height = minimumHitTestHeight;
hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
}
return hitTestingBounds;
}

superview 响应 subview 的事件

这个在限制事件分发到 subviews 上小节里面就有说过。重载 hitTest:withEvent: 方法返回自己 self。

Author

陈昭

Posted on

2017-12-01

Updated on

2021-12-27

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Kommentare

You forgot to set the shortname for Disqus. Please set it in _config.yml.